#!/bin/bash
#==============================================================#
# 脚本名     :   DMS2P
# 创建时间   :   2025-09-05 14:07:00
# 更新时间   :   2025-09-05 17:26:00
# 描述      :   达梦数据库一键切换指定备库为主库
# 路径      :   /soft/DMS2P
# 版本      :   1.0.0
# 作者      :   yuanzijian(yzj@dameng.com)
# Copyright (C) 2025-2025 Zijian Yuan
#==============================================================#
# 操作系统版本
os_version=7
# 达梦软件安装目录，默认 /dm
env_dm_home=/dmdbms
# 定义数据库业务端口
port_num=
# 定义数据库实例目录
data_path=
# 定义业务可以切换为主库的备库数组
declare -a std_nodes
# 全局心跳网络IP数组
declare -a all_malip_arr
# 定义心跳网络和守护进程通信端口关系
declare -A mal_hosts_ports
# 定义业务IP和实例名关联数组
declare -A hosts_inst_names
# 定义业务IP和端口关联数组
declare -A hosts_inst_ports
# 定义数据守护组名
dw_group_name=
#==============================================================#
# 日志输出颜色
#==============================================================#
function color_printf() {
	declare -A color_map=(
		["red"]='\E[31m'
		["green"]='\E[1;32m'
		["yellow"]='\E[33m'
		["blue"]='\E[34m'
		["white"]='\E[37m'
	)
	local res='\E[0m'
	local color=${color_map[$1]:-"$2"}
	printf "${color}%-20s %-30s %-50s\n${res}" "$2" "$3" "$4"
}
#==============================================================#
# 判断传进来的参数
#==============================================================#
function checkpara_null() {
	if [[ -z $2 || $2 == -* ]]; then
		printf "\n"
		color_printf red "请检查参数 [ $1 ] 的值是否为空！"
		printf "\n"
		exit 1
	fi
}
#========================================================
# 工具：execute_sql 以 SYSDBA 用户执行单条 SQL 命令
#========================================================
function execute_sql() {
	local user_name="$1"
	local user_pwd="$2"
	local sql_command="$3"
	# 调用 disql 执行，静默模式 -S -L
	eval "${env_dm_home}/bin/disql -S -L ${user_name}/'\"$user_pwd\"':$port_num -c \"set heading off\" -e \"$sql_command\""
	# 将 disql 的退出码原样返回
	return $?
}
#==============================================================#
# 帮助
#==============================================================#
function help() {
	# 打印参数
	print_options() {
		local options=("$@")
		# 调用 color_printf 函数，输出绿色字体
		# ${option%% *} 表示从 option 变量中删除最后一个空格及其后面的字符，保留前面的部分
		# ${option#* } 表示从 option 变量中删除第一个空格及其前面的字符，保留后面的部分
		for option in "${options[@]}"; do
			color_printf green "${option%% *}" "${option#* }"
		done
	}
	declare -a options=()
	printf "\n"
	color_printf blue "用途: 一键切换指定备库为主库"
	printf "\n"
	color_printf red "用法: DMS2P [选项] 选项值 { COMMAND | help }"
	printf "\n"
	options=(
		"-sp 数据库用户SYSDBA密码"
		"-dpi DW 实[即]时备库公网IP，例如：-dpi 192.168.2.28"
	)
	print_options "${options[@]}"
	printf "\n"
	color_printf red "注意：本脚本在使用之前，建议停止确认监视器，请谨慎使用！！！"
	printf "\n"
	exit 0
}
#==============================================================#
# 获取操作系统类型和版本
#==============================================================#
function getosversion() {
	# 定义 os_version
	libc_version=$(ldd --version | head -n 1 | awk '{print $NF}' | cut -d '.' -f 2)
	# 根据 libc_version 获取 os_version
	local -A os_version_map=(
		["6"]="12-16"
		["7"]="17-27"
		["8"]="28-33"
		["9"]="34-38"
		["10"]="39-99" # redhat10 是 ldd (GNU libc) 2.39
	)
	for value in "${!os_version_map[@]}"; do
		IFS="-" read -r start end <<<"${os_version_map[$value]}"
		if ((libc_version >= start && libc_version <= end)); then
			os_version="$value"
			break
		fi
	done
	# 校验是否匹配到 os_version
	if [[ -z "$os_version" ]]; then
		color_printf red "当前操作系统不在脚本支持列表中，如有需要请联系开发者适配！"
		exit 1
	fi
}
#==============================================================#
# 获取数据库部署目录
#==============================================================#
function getdbinfo() {
	# 1. 获取DM_HOME
	if [[ "$os_version" -ne 6 ]]; then
		env_dm_home=$(pgrep -af "dmap|dmserver" | awk '{print $2}' | xargs dirname | awk -F'/bin' '{print $1}' | head -1)
	else
		# shellcheck disable=SC2009
		env_dm_home=$(ps -ef | grep "dmap\|dmserver" | grep -v grep | awk '{split($8, a, "/bin"); print a[1]}' | head -1)
	fi
	# 导入数据库安装目录
	export DM_HOME="${env_dm_home}"
	export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$DM_HOME/bin"
	# 2. 获取DATA_PATH路径
	if [[ "$os_version" -ne 6 ]]; then
		data_path=$(pgrep -af "dmserver" | awk -F'=' '{print $2}' | xargs -I {} dirname {} | head -1)
	else
		# shellcheck disable=SC2009
		data_path=$(ps -ef | grep "dmserver" | grep -v grep | awk -F'=' '{print $2}' | xargs -I {} dirname {} | head -1)
	fi
	if [[ -z $data_path ]]; then
		printf "\n"
		color_printf red "注意：数据库没有正常启动，请检查数据库运行状态！！！"
		printf "\n"
		exit 1
	fi
}
#==============================================================#
# 获取数据库配置文件信息
#==============================================================#
function getdbconf() {
	local -a all_pubip_arr mal_inst_port_arr mal_dw_port_arr mal_inst_name_arr
	# 备份并转换大写（保留原功能）
	sed -i.bak '/\//! s/.*/\U&/; s/MAL_TEMP_PATH/\U&/Ig' "${data_path}"/dmmal.ini
	# 逐行处理文件,将对应的值写入到数组里面
	while IFS= read -r line; do
		# 分割键值对（去除前后空格）
		IFS='=' read -r key value <<<"$line"
		key=$(echo "$key" | xargs)
		value=$(echo "$value" | xargs)
		case "$key" in
		MAL_INST_HOST)
			all_pubip_arr+=("$value")
			;;
		MAL_INST_PORT)
			mal_inst_port_arr+=("$value")
			;;
		MAL_HOST)
			all_malip_arr+=("$value")
			;;
		MAL_DW_PORT)
			mal_dw_port_arr+=("$value")
			;;
		MAL_INST_NAME)
			mal_inst_name_arr+=("$value")
			;;
		esac
	done <"${data_path}"/dmmal.ini
	# 关联数组添加元素
	for ((i = 0; i < ${#all_pubip_arr[@]}; i++)); do
		hosts_inst_ports["${all_pubip_arr[$i]}"]="${mal_inst_port_arr[$i]}"
		hosts_inst_names["${all_pubip_arr[$i]}"]="${mal_inst_name_arr[$i]}"
		mal_hosts_ports["${all_malip_arr[$i]}"]="${mal_dw_port_arr[$i]}"
	done
}
#==============================================================#
# 配置监视器
#==============================================================#
function dw_monitor() {
	local oguid config
	# 获取数据库业务端口
	port_num=${hosts_inst_ports[$db_public_ip]}
	# 校验sysdba密码
	db_status=$(execute_sql "SYSDBA" "$sysdba_pwd" "select status\\\$ from v\\\$instance;" | tail -n 1)
	[[ $db_status == "OPEN" ]] || {
		printf "\n"
		color_printf red "数据库密码： ${sysdba_pwd} 不正确，请重新制定 -sp 参数"
		printf "\n"
	}
	# 获取GRP组名
	dw_group_name=$(execute_sql "SYSDBA" "$sysdba_pwd" "select GROUP_NAME from v\\\$dmwatcher;" | tail -n 1)
	# 获取OGUID值
	oguid=$(execute_sql "SYSDBA" "$sysdba_pwd" "select OGUID from v\\\$instance;" | tail -n 1)
	# 生成 dmmonitor.ini 基础配置
	config=$(
		cat <<EOF
MON_DW_CONFIRM = 0
MON_LOG_PATH = $env_dm_home/log
MON_LOG_INTERVAL = 60
MON_LOG_FILE_SIZE = 32
MON_LOG_SPACE_LIMIT = 2048
[$dw_group_name]
MON_INST_OGUID = $oguid
EOF
	)
	# 循环追加 MON_DW_IP 配置
	for ((i = 0; i < ${#all_malip_arr[@]}; i++)); do
		config+=$(
			cat <<EOF

MON_DW_IP = ${all_malip_arr[$i]}:${mal_hosts_ports[${all_malip_arr[$i]}]}
EOF
		)
	done
	# 一次性写入 dmmonitor.ini
	printf "%s\n" "$config" | tee "${data_path}"/dmmonitor_"${dw_group_name}".ini >/dev/null
}
#==============================================================#
# 判断指定备库是否在允许切换主库的列表中
#==============================================================#
function check_std() {
	# 使用 mapfile 读取命令输出到数组
	declare -a std_nodes
	# 读取每一行到数组中
	output="$(
		"$env_dm_home"/bin/dmmonitor "${data_path}"/dmmonitor_"${dw_group_name}".ini <<'EOF' | awk '/choose/{flag=1; next} flag && /^[0-9]+:/ {print $2}'
choose switchover
exit
EOF
	)"
	while IFS= read -r line; do
		std_nodes+=("$line")
	done <<<"$output"
	# 判断是否在可切换备库中
	[[ ${std_nodes[*]} =~ ${hosts_inst_names[${db_public_ip}]} ]] || {
		printf "\n"
		color_printf red "注意：当前指定节点：$db_public_ip 不是可切换主库的备库，请重新指定：${std_nodes[*]} 所在节点IP！！！"
		printf "\n"
		exit 1
	}
}
#==============================================================#
# 切换备库为主库
#==============================================================#
function switch_pri() {
	printf "\n"
	expect -c "
set timeout 60
spawn ${env_dm_home}/bin/dmmonitor ${data_path}/dmmonitor_${dw_group_name}.ini
puts \"\";
sleep 3;
send \"login\r\"
expect {
  -re \"(用户名|username):\" { 
      send \"SYSDBA\r\"
      exp_continue 
  }
  -re \"(密码|password):\" { 
      send \"$sysdba_pwd\r\"
      exp_continue 
  }
  -re \"(登录监视器成功|Login dmmonitor success)\" {
      send \"switchover ${dw_group_name}.${hosts_inst_names[$db_public_ip]}\r\"
      exp_continue
  }
  -re \"(YES/NO/Y/N)\" {
      send \"Y\r\"
      exp_continue
  }
  -re \"(RECOVERY-->OPEN)\" {
      sleep 3
      send \"exit\r\"
      exp_continue
  }
  eof {
      exit 0
  }
  timeout {
      send \"exit\r\"
      exit 1
  }
 }"
}
#==============================================================#
# 脚本入口
#==============================================================#
function main() {
	while [[ $1 ]]; do
		case $1 in
		-dpi | --db_public_ip)
			checkpara_null "$1" "$2"
			db_public_ip=$2
			shift 2
			;;
		-sp | --sysdba_pwd)
			checkpara_null "$1" "$2"
			sysdba_pwd=$2
			shift 2
			;;
		-h | --help) help ;;
		*)
			printf "\n"
			color_printf red "脚本已退出安装，请检查是否参数是否正确，可以查看脚本帮助：'DMS2P --help' 获取更多信息！"
			printf "\n"
			exit 1
			;;
		esac
	done
	# 检查 expect 是否已安装
	if ! command -v expect >/dev/null 2>&1; then
		printf "\n"
		color_printf red "错误：系统未安装 expect 工具包，无法执行自动化交互操作"
		printf "\n"
		color_printf yellow "请根据您的系统类型执行以下安装命令："
		printf "\n"
		printf "    # CentOS/RHEL:\n"
		printf "    yum install expect -y\n"
		printf "\n"
		printf "    # Ubuntu/Debian:\n"
		printf "    apt-get install expect -y\n"
		printf "\n"
		exit 1
	fi
	# 执行切换命令
	getosversion
	getdbinfo
	getdbconf
	dw_monitor
	check_std
	switch_pri
}
main "$@"
